package com.liuqianfei.markme.rules;

import com.liuqianfei.markme.core.MarkMeException;
import com.liuqianfei.util.StringUtil;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * class Ruler
 *
 * Helper class, used by [[MarkdownIt#core]], [[MarkdownIt#block]] and
 * [[MarkdownIt#inline]] to manage sequences of functions (rules):
 *
 * - keep rules in defined order
 * - assign the name to each rule
 * - enable/disable rules
 * - add/replace rules
 * - allow assign rules to additional named chains (in the same)
 * - cacheing lists of active rules
 *
 * You will not need use this class directly until write plugins. For simple
 * rules control use [[MarkdownIt.disable]], [[MarkdownIt.enable]] and
 * [[MarkdownIt.use]].
 **/
public class Ruler
{
    /**
     * List of added rules
     */
    private List<Rule> rules = new ArrayList<>();
    
    /**
     * Cached rule chains.
     * <ui>
     * <li>First level - chain name, '' for default.</li>
     * <li>Second level - diginal anchor for fast filtering by charcodes.</li>
     * </ul>
     */
    private Map<String, List<IAction>> cache;

    ////////////////////////////////////////////////////////////////////////////////
    //Helper methods, should not be used directly

    // Find rule index by name
    private int find(String ruleName)
    {
        for (int i = 0; i < rules.size(); i++)
        {
            if (ruleName.equals(rules.get(i).name))
            {
                return i;
            }
        }
        return -1;
    }

    // Build rules lookup cache
    //
    private void compile()
    {
        List<String> chains = new ArrayList<>();
        chains.add("");

        List<String> alts;

        // collect unique names
        for (Rule rule : rules)
        {
            if (!rule.enabled)
                continue;

            alts = rule.alts;

            if (null == alts)
                continue;

            for (String alt : alts)
            {
                if (!chains.contains(alt))
                    chains.add(alt);
            }
        }

        cache = new HashMap<>();

        for (String chain : chains)
        {
            List<IAction> fns = new ArrayList<>();
            cache.put(chain, null);
            for (Rule rule : rules)
            {
                if (!rule.enabled)
                    continue;

                if (StringUtil.isNotEmpty(chain) && !rule.alts.contains(chain))
                    continue;

                fns.add(rule.fn);
                cache.put(chain, fns);
            }
        }
    }

    /**
     * Ruler.at(name, fn [, options])
     * - name (String): rule name to replace.
     * - fn (Function): new rule function.
     * - options (Object): new rule options (not mandatory).
     *
     * Replace rule by name with new function & options. Throws error if name not
     * found.
     *
     * ##### Options:
     *
     * - __alt__ - array with names of "alternate" chains.
     *
     * ##### Example
     *
     * Replace existing typorgapher replacement rule with new one:
     *
     * ```javascript
     * var md = require('markdown-it')();
     *
     * md.core.ruler.at('replacements', function replace(state) {
     *   //...
     * });
     * ```
     **/
    public void at(String ruleName, IAction fn, Rule options) throws MarkMeException
    {
        int index = this.find(ruleName);
        if (null == options)
            options = new Rule();

        if (index == -1)
            throw new MarkMeException("Parser rule not found: " + ruleName);

        Rule rule = rules.get(index);
        rule.fn = fn;
        rule.alts = options.alts;
        rules.set(index, rule);

        cache = null;
    }

    /**
     * Ruler.before(beforeName, ruleName, fn [, options])
     * - beforeName (String): new rule will be added before this one.
     * - ruleName (String): name of added rule.
     * - fn (Function): rule function.
     * - options (Object): rule options (not mandatory).
     *
     * Add new rule to chain before one with given name. See also
     * [[Ruler.after]], [[Ruler.push]].
     *
     * ##### Options:
     *
     * - __alt__ - array with names of "alternate" chains.
     *
     * ##### Example
     *
     * ```javascript
     * var md = require('markdown-it')();
     *
     * md.block.ruler.before('paragraph', 'my_rule', function replace(state) {
     *   //...
     * });
     * ```
     **/
    public void before(String beforeName, String ruleName, IAction fn, Rule options) throws MarkMeException
    {
        int index = find(beforeName);

        if (null == options)
            options = new Rule();

        if (index == -1)
            throw new MarkMeException("Parser rule not found: " + beforeName);

        Rule rule = new Rule(ruleName, true, fn, options.alts);
        rules.add(index, rule);

        cache = null;
    }

    /**
     * Ruler.after(afterName, ruleName, fn [, options])
     * - afterName (String): new rule will be added after this one.
     * - ruleName (String): name of added rule.
     * - fn (Function): rule function.
     * - options (Object): rule options (not mandatory).
     *
     * Add new rule to chain after one with given name. See also
     * [[Ruler.before]], [[Ruler.push]].
     *
     * ##### Options:
     *
     * - __alt__ - array with names of "alternate" chains.
     *
     * ##### Example
     *
     * ```javascript
     * var md = require('markdown-it')();
     *
     * md.inline.ruler.after('text', 'my_rule', function replace(state) {
     *   //...
     * });
     * ```
     **/
    public void after(String afterName, String ruleName, IAction fn, Rule options) throws MarkMeException
    {
        int index = find(afterName);

        if (null == options)
            options = new Rule();

        if (index == -1)
            throw new MarkMeException("Parser rule not found: " + afterName);

        Rule rule = new Rule(ruleName, true, fn, options.alts);
        rules.add(index + 1, rule);

        cache = null;
    }

    /**
     * Ruler.push(ruleName, fn [, options])
     * - ruleName (String): name of added rule.
     * - fn (Function): rule function.
     * - options (Object): rule options (not mandatory).
     *
     * Push new rule to the end of chain. See also
     * [[Ruler.before]], [[Ruler.after]].
     *
     * ##### Options:
     *
     * - __alt__ - array with names of "alternate" chains.
     *
     * ##### Example
     *
     * ```javascript
     * var md = require('markdown-it')();
     *
     * md.core.ruler.push('my_rule', function replace(state) {
     *   //...
     * });
     * ```
     **/
    public void push(String ruleName, IAction fn, Rule options)
    {
        if (null == options)
            options = new Rule();

        Rule rule = new Rule(ruleName, true, fn, options.alts);
        rules.add(rule);

        cache = null;
    }

    /**
     * Ruler.enable(list [, ignoreInvalid]) -> Array
     * - list (String|Array): list of rule names to enable.
     * - ignoreInvalid (Boolean): set `true` to ignore errors when rule not found.
     *
     * Enable rules with given names. If any rule name not found - throw Error.
     * Errors can be disabled by second param.
     *
     * Returns list of found rule names (if no exception happened).
     *
     * See also [[Ruler.disable]], [[Ruler.enableOnly]].
     **/
    public List<String> enable(List<String> list, boolean ignoreInvalid) throws MarkMeException
    {
        List<String> result = new ArrayList<>();

        // Search by name and enable
        for (String name : list)
        {
            int idx = find(name);

            if (idx < 0 && ignoreInvalid)
                throw new MarkMeException("Rules manager: invalid rule name " + name);

            Rule rule = rules.get(idx);
            rule.enabled = true;

            result.add(name);
        }

        cache = null;
        return result;
    }

    /**
     * Ruler.enableOnly(list [, ignoreInvalid])
     * - list (String|Array): list of rule names to enable (whitelist).
     * - ignoreInvalid (Boolean): set `true` to ignore errors when rule not found.
     *
     * Enable rules with given names, and disable everything else. If any rule name
     * not found - throw Error. Errors can be disabled by second param.
     *
     * See also [[Ruler.disable]], [[Ruler.enable]].
     **/
    public void enableOnly(List<String> list, boolean ignoreInvalid) throws MarkMeException
    {
        for (Rule rule : rules)
        {
            rule.enabled = false;
        }

        this.enable(list, ignoreInvalid);
    }

    /**
     * Ruler.disable(list [, ignoreInvalid]) -> Array
     * - list (String|Array): list of rule names to disable.
     * - ignoreInvalid (Boolean): set `true` to ignore errors when rule not found.
     *
     * Disable rules with given names. If any rule name not found - throw Error.
     * Errors can be disabled by second param.
     *
     * Returns list of found rule names (if no exception happened).
     *
     * See also [[Ruler.enable]], [[Ruler.enableOnly]].
     **/
    public List<String> disable(List<String> list, boolean ignoreInvalid) throws MarkMeException
    {
        List<String> result = new ArrayList<>();

        // Search by name and disable
        for (String name : list)
        {
            int idx = find(name);

            if (idx < 0 && ignoreInvalid)
                throw new MarkMeException("Rules manager: invalid rule name " + name);

            Rule rule = rules.get(idx);
            rule.enabled = false;

            result.add(name);
        }

        cache = null;
        return result;
    }

    /**
     * Ruler.getRules(chainName) -> Array
     *
     * Return array of active functions (rules) for given chain name. It analyzes
     * rules configuration, compiles caches if not exists and returns result.
     *
     * Default chain name is `''` (empty string). It can't be skipped. That's
     * done intentionally, to keep signature monomorphic for high speed.
     **/
    public List<IAction> getRules(String chainName)
    {
        if (null == cache)
            compile();

        // Chain can be empty, if rules disabled. But we still have to return Array.
        return cache.get(chainName);
    }
}
